package com.sinochem.yunlian.ship.common.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
import redis.clients.jedis.exceptions.JedisException;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author: houshihao
 * @description:  Redis 分布式锁
 * @date: 2018/3/12
 * @modified By :
 * @version:
*/
@Component
@Slf4j
public class DistributedLockService {

	/**
	 * 获取redis分布式锁 ,catchKey有效期默认为{expireMs}
	 * @param cacheKey 分布式锁key
	 * @return 如果锁成功返回 返回key对应的value，否则返回null
	 */
	public String tryLock(String cacheKey) {
		return this.tryLock(cacheKey, expireMs);
	}

	/**
	 * 获取cacheKey的redis分布式锁 ,catchKey有效期设置为expireMilliS
	 * @param cacheKey  分布式锁key
	 * @param expireMs 分布式锁 有效期
	 * @return 如果锁成功返回 返回key对应的value，否则返回null
	 */
	public String  tryLock(String cacheKey,long expireMs) {
		String lockKey = lockKeyPrefix + cacheKey;
		String lockValue = UUID.randomUUID().toString(); // 生成随机value

		boolean locked = false;
		try {
			// 1、尝试获取并发锁
			locked = this.setIfNotExist(lockKey, lockValue, expireMs);
			// 2、 若当前不能获取锁资源，阻塞，定时重试
			if (!locked) {
				int times = 0;
				while (times++ < lockRetryTimes && !locked && lockRetrySpan > 0) {
					log.warn("获取key[{}]更新锁失败，第[{}]次重试加锁", cacheKey, times);
					Thread.sleep(lockRetrySpan);
					locked = this.setIfNotExist(lockKey, lockValue, expireMs);
				}
			}
		} catch (JedisException e) {
			log.error("--获取key[{}]更新锁异常,redis降级处理--", cacheKey, e);
		} catch (Exception e) {
			log.warn("获取key[{}]更新锁异常", cacheKey, e);
		}

		return locked ? lockValue : null;
	}

	/**
	 * 解除key为value的锁
	 *
	 * @param key
	 * @param value
	 * @return
	 */
	public Boolean unlock(String key,String value) {
		String lockKey = lockKeyPrefix + key;
		return (Boolean)redisTemplate.execute((RedisCallback<Boolean>) connection -> {
			Object nativeConnection = connection.getNativeConnection();
			Long result = 0L;

			List<String> keys = new ArrayList<>();
			keys.add(lockKey);
			List<String> values = new ArrayList<>();
			values.add(value);

			// 集群模式
			if (nativeConnection instanceof JedisCluster) {
				result = (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, values);
			}

			// 单机模式
			if (nativeConnection instanceof Jedis) {
				result = (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, values);
			}

			boolean locked = (result == 1);
			if (!locked) {
				log.warn("Redis分布式锁，解锁{}失败！", lockKey);
			}

			return locked;
		});
	}

	/**
	 * <p>
	 * 命令 SET resource-name anystring NX PX max-lock-time 是一种在 Redis 中实现锁的简单方法。
	 * <p>
	 * 客户端执行以上的命令：
	 * <p>
	 * 如果服务器返回 OK ，那么这个客户端获得锁。
	 * 如果服务器返回 NIL ，那么客户端获取锁失败，可以在稍后再重试。
	 *h
	 * @param key     锁的Key
	 * @param value   锁的值
	 * @param expireMs 失效时间（毫秒）
	 * @return
	 */
	private Boolean setIfNotExist(final String key, final String value, final long expireMs) {
		long now = System.currentTimeMillis();
		return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
			Object nativeConnection = connection.getNativeConnection();
			String result = null;
			if (nativeConnection instanceof JedisCommands) {
				result = ((JedisCommands) nativeConnection).set(key, value, NX, PX, expireMs);
			}
			log.debug("获取锁 key:{},result:{},耗时：{}", key, result, System.currentTimeMillis() - now);
			return OK.equalsIgnoreCase(result);
		});
	}

	/**
	 * 解锁的lua脚本
	 */
	public static final String UNLOCK_LUA;

	static {
		StringBuilder sb = new StringBuilder();
		sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
		sb.append("then ");
		sb.append("    return redis.call(\"del\",KEYS[1]) ");
		sb.append("else ");
		sb.append("    return 0 ");
		sb.append("end ");
		UNLOCK_LUA = sb.toString();
	}

	private final static String lockKeyPrefix = "ship_distLock:";//锁前缀

	/**
	 * 将key 的值设为value ，当且仅当key不存在，等效于 SETNX
	 */
	public static final String NX = "NX";
	/**
	 * 以毫秒为单位设置 key 的过期时间
	 * PX = milliseconds
	 */
	public static final String PX = "PX";

	/**
	 * 分布式锁超时时间,单位：毫秒
	 */
	@Value("${distributedLock.expire:1000}")
	private long expireMs;

	/**
	 * 分布式锁重试次数
	 */
	@Value("${distributedLock.retry.times:3}")
	private int lockRetryTimes;
	/**
	 * 分布式锁重试时间间隔 毫秒
	 */
	@Value("${distributedLock.retry.span:100}")
	private int lockRetrySpan;

	/**
	 * 调用set后的返回值
	 */
	public static final String OK = "OK";

	@Resource
	private RedisTemplate redisTemplate;

}
